use std::rc::Rc;

use cgmath::{vec2, vec3, Matrix4, SquareMatrix, Vector2};
use mark_dirty::DirtyMark;
use wgpu::{include_wgsl, vertex_attr_array};

use crate::math::{RegionRect, Transform};

use super::{
    framework::{
        material::Material,
        mesh::MeshInstance,
        shader::Shader,
        texture::{Pixmap, Texture},
        uniform::Uniform,
        Attribute,
    },
    AsBytes, Vertex,
};

#[repr(C)]
pub struct Vertex2DU {
    position: Vector2<f32>,
    uv: Vector2<f32>,
}

impl AsBytes for Vertex2DU {}
impl Vertex for Vertex2DU {
    const ATTRS: &'static [wgpu::VertexAttribute] =
        &vertex_attr_array![0 => Float32x2, 1 => Float32x2];
}

#[inline]
pub fn v2duv(position: Vector2<f32>, uv: Vector2<f32>) -> Vertex2DU {
    Vertex2DU { position, uv }
}

#[inline]
pub fn v2du(x: f32, y: f32, u: f32, v: f32) -> Vertex2DU {
    v2duv(vec2(x, y), vec2(u, v))
}

pub struct Renderer {
    pub material: Material,
    pub transform: DirtyMark<Transform>,
    mesh: MeshInstance<Vertex2DU>,
    aspect: f32,
}

impl Renderer {
    pub fn new(
        ctx: &super::Context,
        global_layout: &wgpu::BindGroupLayout,
        pixmap: Pixmap,
    ) -> Self {
        Self::with_region(
            ctx,
            global_layout,
            pixmap,
            RegionRect::new(0.0, 0.0, 1.0, 1.0),
        )
    }

    pub fn with_region(
        ctx: &super::Context,
        global_layout: &wgpu::BindGroupLayout,
        pixmap: Pixmap,
        region: RegionRect,
    ) -> Self {
        let aspect = pixmap.width as f32 / pixmap.height as f32;

        let uniform = Uniform::new(ctx, wgpu::ShaderStages::VERTEX, Matrix4::identity());
        let texture = Texture::new(ctx, wgpu::ShaderStages::FRAGMENT, pixmap);
        let pipeline_layout =
            ctx.create_pipeline_layout(&[global_layout, uniform.layout(), texture.layout()]);

        let mut shader = Shader::new(ctx, include_wgsl!("../../shaders/sprite.wgsl"));
        shader.add_buffer_layout::<Vertex2DU>();
        shader.add_screen_color_target(ctx);

        let pipeline = ctx.create_simple_render_pipeline(
            &pipeline_layout,
            &shader,
            wgpu::PrimitiveState {
                front_face: wgpu::FrontFace::Cw,
                cull_mode: Some(wgpu::Face::Back),
                ..Default::default()
            },
        );

        let mut material = Material::new(Rc::new(pipeline));
        material.put(1, Box::new(uniform));
        material.put(2, Box::new(texture));

        let mut transform = DirtyMark::new(Transform::default());
        transform.scale = vec3(aspect, 1.0, 1.0);

        let mut mesh = MeshInstance::new(ctx);
        mesh.add_quad([
            v2du(-0.5, -0.5, region.left(), region.bottom()),
            v2du(-0.5, 0.5, region.left(), region.top()),
            v2du(0.5, 0.5, region.right(), region.top()),
            v2du(0.5, -0.5, region.right(), region.bottom()),
        ]);

        Self {
            material,
            aspect,
            transform,
            mesh,
        }
    }

    pub fn set_scale(&mut self, scale: f32) {
        self.transform.scale = vec3(scale * self.aspect, scale, 1.0)
    }

    pub fn prepare(&mut self, ctx: &super::Context) {
        if self.transform.is_dirty() {
            *self
                .material
                .attr_mut::<Uniform<Matrix4<f32>>>(1)
                .unwrap()
                .data_mut() = self.transform.combination();
            self.transform.unmark_dirty();
        }
        self.material.prepare(ctx);
        self.mesh.prepare(ctx);
    }

    pub fn render<'p>(&'p self, pass: &mut wgpu::RenderPass<'p>) {
        self.material.set(pass);
        self.mesh.render(pass);
    }
}
